package com.piiworks.google;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;

import org.apache.log4j.Logger;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.Namespace;
import org.jdom2.input.SAXBuilder;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;

/**
 * Service to create a KML output stream from a gpx file
 * 
 * @author gerliczky
 * 
 */
public class Gpx2kmlService
{
	private static final String GPXTRACKTAG = "trk";
	private static final String GPXTRACKPOINTSEGMENTTAG = "trkseg";
	private static final String GPXTRACKPOINTTAG = "trkpt";

	private static final String GPXLONGITUDINALATTRIBUTE = "lon";
	private static final String GPXLATITUDINALATTRIBUTE = "lat";

	private static final String GPXWAYPOINTTAG = "wpt";
	private static final String GPXTIMETAG = "time";
	private static final String GPXDESCRIPTIONTAG = "desc";
	private static final String GPXSYMBOLTAG = "sym";

	private static final String GPXROUTETAG = "rte";
	private static final String GPXROUTEPOINTTAG = "rtept";

	private static final String GPXEXTENSIONTAG = "extensions";
	private static final String GPXEXTENSIONROUTEPOINTEXTENSIONTAG = "RoutePointExtension";
	private static final String GPXEXTENSIONROUTEPOINTTAG = "rpt";

	private static final String GPXROUTESYMBOLNAME = "Navaid";
	private static final String GPXROUTEYELLOW = "Navaid, Amber";
	private static final String GPXROUTERED = "Navaid, Red";
	private static final String GPXROUTEGREEN = "Navaid, Green";
	private static final String GPXROUTEBLUE = "Navaid, Blue";
	private static final String GPXROUTEBLACK = "Navaid, Black";

	private static final Namespace GPXNS = Namespace.getNamespace("http://www.topografix.com/GPX/1/1");
	private static final Namespace GPXXNS = Namespace.getNamespace("http://www.garmin.com/xmlschemas/GpxExtensions/v3");

	private List<Element> gpxRouteWayPointList = new ArrayList<Element>();

	private Element gpxRootElement;

	private static final String NAMETAG = "name";

	private static final String KMLTAG = "kml";
	private static final String KMLDOCTAG = "Document";
	private static final String KMLDESCRIPTIONTAG = "description";

	private static final String KMLSTYLETAG = "Style";
	private static final String KMLICONSTYLETAG = "IconStyle";
	private static final String KMLICONURLTAG = "Icon";
	private static final String KMLHREFTAG = "href";
	private static final String KMLLINESTYLETAG = "LineStyle";
	private static final String KMLCOLORTAG = "color";
	private static final String KMLWIDTHTAG = "width";

	private static final String KMLFOLDERTAG = "Folder";
	private static final String KMLOPENTAG = "open";

	private static final String KMLPLACETAG = "Placemark";
	private static final String KMLSTYLEURLTAG = "styleUrl";
	private static final String KMLPOINTTAG = "Point";
	private static final String KMLCOORDINATESTAG = "coordinates";

	private static final String KMLLINESTRINGTAG = "LineString";

	private static final Namespace KMLNS = Namespace.getNamespace("http://www.opengis.net/kml/2.2");
	private static final String KMLICONNAME = "pointIcon";
	private static final String KMLICONURL = "http://maps.google.com/mapfiles/kml/paddle/red-circle.png";

	private Document kmlDoc;
	private Element kmlDocElement = new Element(KMLDOCTAG, KMLNS);

	private final static SimpleDateFormat GPXDESCRIPTIONDATETIMEFORMATTER = new SimpleDateFormat("yyyy.MM.dd. HH:mm:ss");
	private final static SimpleDateFormat GPXTRACKPOINTDATETIMEFORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");

	private static enum RouteEnum
	{
		yellowRoute, redRoute, greenRoute, blueRoute, blackRoute
	};

	private String inGpxName;

	private static Logger logger = Logger.getLogger(Gpx2kmlService.class);

	/**
	 * Creates a KML output stream from a GPX file even if it is drawn by hand or tracked by a device
	 * 
	 * @param inGpxFile input GPX file
	 * @param kmlOutStream the KML output stream
	 * @return
	 * @throws JDOMException
	 * @throws IOException
	 */
	public void convert(final File inGpxFile, final OutputStream kmlOutputStream) throws JDOMException, IOException
	{
		// TODO remove id_ name prefix, remove drive letter, normalize name conversion
		inGpxName = inGpxFile.getName().substring(0, inGpxFile.getName().lastIndexOf("."));

		gpxRootElement = new SAXBuilder().build(inGpxFile).getRootElement();
		Element kmlRootElement = new Element(KMLTAG, KMLNS);
		kmlRootElement.addContent(kmlDocElement);
		kmlDoc = new Document(kmlRootElement);

		// add style elements
		Element styleElement = new Element(KMLSTYLETAG, KMLNS);
		styleElement.setAttribute("id", KMLICONNAME);
		Element iconStyleElement = new Element(KMLICONSTYLETAG, KMLNS);
		Element iconElement = new Element(KMLICONURLTAG, KMLNS);
		Element hrefElemenet = new Element(KMLHREFTAG, KMLNS);
		hrefElemenet.setText(KMLICONURL);
		iconElement.addContent(hrefElemenet);
		iconStyleElement.addContent(iconElement);
		styleElement.addContent(iconStyleElement);
		kmlDocElement.addContent(styleElement);

		kmlDocElement.addContent(lineStyleElement(RouteEnum.yellowRoute));
		kmlDocElement.addContent(lineStyleElement(RouteEnum.redRoute));
		kmlDocElement.addContent(lineStyleElement(RouteEnum.greenRoute));
		kmlDocElement.addContent(lineStyleElement(RouteEnum.blueRoute));
		kmlDocElement.addContent(lineStyleElement(RouteEnum.blackRoute));

		// filter and add waypoints
		for (Element element : gpxRootElement.getChildren(GPXWAYPOINTTAG, GPXNS))
		{

			if (gpxRootElement.getChild(GPXTRACKTAG, GPXNS) != null)
			{
				element = replaceTimeWaypointCoordinates(element);
			}
			if (element.getChildText(GPXSYMBOLTAG, GPXNS).contains(GPXROUTESYMBOLNAME))
			{
				gpxRouteWayPointList.add(element);
			}
			else
			{
				kmlDocElement.addContent(createKMLPlacemark(element));
			}
		}

		// add coordinates
		Element folderElement = new Element(KMLFOLDERTAG, KMLNS);
		Element nameElement = new Element(NAMETAG, KMLNS);
		nameElement.setText(inGpxName);
		folderElement.addContent(nameElement);

		Element descriptionElement = new Element(KMLDESCRIPTIONTAG, KMLNS);
		descriptionElement.setText(inGpxName);
		folderElement.addContent(descriptionElement);

		Element placemarkElement = null;
		Element lineElement = null;
		Element coordinatesElement = null;
		StringBuilder coordinates = new StringBuilder();

		Element lastPointElement = null;

		// in case of drawn route
		if (gpxRootElement.getChild(GPXROUTETAG, GPXNS) != null)
		{

			//TODO handle more rte tags
			gpxRoutepointLoop: for (Element routePointElement : gpxRootElement.getChild(GPXROUTETAG, GPXNS).getChildren(GPXROUTEPOINTTAG, GPXNS))
			{
				for (Element routeWaypointElement : gpxRouteWayPointList)
				{
					if (routePointElement.getAttributeValue(GPXLONGITUDINALATTRIBUTE).equals(routeWaypointElement.getAttributeValue(GPXLONGITUDINALATTRIBUTE)) && routePointElement.getAttributeValue(GPXLATITUDINALATTRIBUTE).equals(routeWaypointElement.getAttributeValue(GPXLATITUDINALATTRIBUTE)))
					{
						placemarkElement = new Element(KMLPLACETAG, KMLNS);
						folderElement.addContent(placemarkElement);

						addPlacemarkContent(placemarkElement, routeWaypointElement);
						if (coordinatesElement != null)
						{
							coordinatesElement.addContent(coordinates.toString());
						}
						lineElement = new Element(KMLLINESTRINGTAG, KMLNS);
						coordinatesElement = new Element(KMLCOORDINATESTAG, KMLNS);
						coordinates.delete(0, coordinates.length());
						if (lastPointElement != null)
						{
							coordinates.append(coordinatesStringFromExtension(lastPointElement));
						}
						coordinates.append(coordinatesStringFromExtension(routePointElement));
						lastPointElement = routePointElement.clone();
						lineElement.addContent(coordinatesElement);
						placemarkElement.addContent(lineElement);
						gpxRouteWayPointList.remove(routeWaypointElement);
						continue gpxRoutepointLoop;
					}
				}
				// there is no start flag
				try
				{
					coordinates.append(coordinatesStringFromExtension(routePointElement));
					lastPointElement = routePointElement.clone();
				}
				catch (NullPointerException e)
				{
					logger.error("There is no start flag\n", e);
				}
			}
		}
		else
		{
			// in case of route is tracked
			// TODO handle more trk or trkseg tag
			gpxTrackpointLoop: for (Element trackPointElement : gpxRootElement.getChild(GPXTRACKTAG, GPXNS).getChild(GPXTRACKPOINTSEGMENTTAG, GPXNS).getChildren(GPXTRACKPOINTTAG, GPXNS))
			{
				for (Element routeWaypointElement : gpxRouteWayPointList)
				{
					if (trackPointElement.getAttributeValue(GPXLONGITUDINALATTRIBUTE).equals(routeWaypointElement.getAttributeValue(GPXLONGITUDINALATTRIBUTE)) && trackPointElement.getAttributeValue(GPXLATITUDINALATTRIBUTE).equals(routeWaypointElement.getAttributeValue(GPXLATITUDINALATTRIBUTE)))
					{
						placemarkElement = new Element(KMLPLACETAG, KMLNS);
						folderElement.addContent(placemarkElement);

						addPlacemarkContent(placemarkElement, routeWaypointElement);
						if (coordinatesElement != null)
						{
							coordinatesElement.addContent(coordinates.toString());
						}
						lineElement = new Element(KMLLINESTRINGTAG, KMLNS);
						coordinatesElement = new Element(KMLCOORDINATESTAG, KMLNS);
						coordinates.delete(0, coordinates.length());
						if (lastPointElement != null)
						{
							coordinates.append("\t\t\t").append(convertCoordinates(lastPointElement)).append("\n");
						}
						coordinates.append("\t\t\t").append(convertCoordinates(trackPointElement)).append("\n");
						lastPointElement = trackPointElement.clone();
						lineElement.addContent(coordinatesElement);
						placemarkElement.addContent(lineElement);
						gpxRouteWayPointList.remove(routeWaypointElement);
						continue gpxTrackpointLoop;
					}
				}
				// there is no start flag
				try
				{
					coordinates.append("\t\t\t").append(convertCoordinates(trackPointElement)).append("\n");
					lastPointElement = trackPointElement.clone();
				}
				catch (NullPointerException e)
				{
					logger.error("There is no start flag\n", e);
				}
			}
		}
		if (coordinatesElement == null)
		{
			coordinatesElement = new Element(KMLCOORDINATESTAG, KMLNS);

			lineElement = new Element(KMLLINESTRINGTAG, KMLNS);
			lineElement.addContent(coordinatesElement);

			placemarkElement = new Element(KMLPLACETAG, KMLNS);
			placemarkElement.addContent(lineElement);
			folderElement.addContent(placemarkElement);

			addPlacemarkContent(placemarkElement, null);
		}
		coordinatesElement.addContent(coordinates.toString());

		kmlDocElement.addContent(folderElement);

		XMLOutputter xmlOutput = new XMLOutputter();

		xmlOutput.setFormat(Format.getPrettyFormat());
		xmlOutput.output(kmlDoc, kmlOutputStream);
	}

	/**
	 * Add content to a place mark represented by a waypoint in GPX
	 * 
	 * @param placemarkElement the place mark element
	 * @param routeWaypointElement The way point element in the GPX representing the place mark
	 */
	private void addPlacemarkContent(Element placemarkElement, Element routeWaypointElement)
	{
		String routeStyle;

		Element routeStyleElement = new Element(KMLSTYLEURLTAG, KMLNS);

		// find style
		if (routeWaypointElement == null)
		{
			routeStyle = RouteEnum.blackRoute.name();
		}
		else if (GPXROUTEYELLOW.equals(routeWaypointElement.getChildText(GPXSYMBOLTAG, GPXNS)))
		{
			routeStyle = RouteEnum.yellowRoute.name();
		}
		else if (GPXROUTERED.equals(routeWaypointElement.getChildText(GPXSYMBOLTAG, GPXNS)))
		{
			routeStyle = RouteEnum.redRoute.name();
		}
		else if (GPXROUTEGREEN.equals(routeWaypointElement.getChildText(GPXSYMBOLTAG, GPXNS)))
		{
			routeStyle = RouteEnum.greenRoute.name();
		}
		else if (GPXROUTEBLUE.equals(routeWaypointElement.getChildText(GPXSYMBOLTAG, GPXNS)))
		{
			routeStyle = RouteEnum.blueRoute.name();
		}
		else if (GPXROUTEBLACK.equals(routeWaypointElement.getChildText(GPXSYMBOLTAG, GPXNS)))
		{
			routeStyle = RouteEnum.blackRoute.name();
		}
		else
		{
			routeStyle = RouteEnum.blackRoute.name();
		}
		routeStyleElement.addContent("#" + routeStyle);
		placemarkElement.addContent(routeStyleElement);

		Element routeNameElement = new Element(NAMETAG, KMLNS);
		routeNameElement.setText(routeStyle);
		placemarkElement.addContent(routeNameElement);

		Element routeDescriptionElement = new Element(KMLDESCRIPTIONTAG, KMLNS);
		routeDescriptionElement.addContent(routeStyle);
		placemarkElement.addContent(routeDescriptionElement);
	}

	/**
	 * Creates a tabbed coordinate line from the route point coordinates
	 * 
	 * @param routePointElement representing the coordinates
	 * @return the coordinate line
	 */
	private String coordinatesStringFromExtension(Element routePointElement)
	{
		StringBuilder returnString = new StringBuilder();
		returnString.append("\t\t\t").append(convertCoordinates(routePointElement)).append("\n");
		for (Element element : routePointElement.getChild(GPXEXTENSIONTAG, GPXNS).getChild(GPXEXTENSIONROUTEPOINTEXTENSIONTAG, GPXXNS).getChildren(GPXEXTENSIONROUTEPOINTTAG, GPXXNS))
		{
			returnString.append("\t\t\t").append(convertCoordinates(element)).append("\n");
		}
		return returnString.toString();
	}

	/**
	 * Search the GPX trackpoint which belongs to the waypoint based on datetime in the description. The datetime format
	 * in the waypoint description should be yyyy.MM.dd. HH:mm:ss The coordinates of the waypoint is changed to the
	 * coordinates of the trackpoint to be able to identify it.
	 * 
	 * @param gpxWaypointElement the waypoint which coordinates should be changed
	 * @return
	 */
	private Element replaceTimeWaypointCoordinates(final Element gpxWaypointElement)
	{
		String desc = gpxWaypointElement.getChildText(GPXDESCRIPTIONTAG, GPXNS);
		logger.info("desc: " + desc);
		Date gpxWaypointDate;
		try
		{
			gpxWaypointDate = GPXDESCRIPTIONDATETIMEFORMATTER.parse(desc);
			Calendar cal = Calendar.getInstance();
			cal.setTime(gpxWaypointDate);
			cal.add(Calendar.HOUR_OF_DAY, -1);
			gpxWaypointDate = cal.getTime();
		}
		catch (ParseException e)
		{
			throw new RuntimeException(desc + " is not a valid date format. " + GPXDESCRIPTIONDATETIMEFORMATTER.toPattern() + " Tag is handled as a normal placemark.\n" + e);
		}

		for (Element element : gpxRootElement.getChild(GPXTRACKTAG, GPXNS).getChild(GPXTRACKPOINTSEGMENTTAG, GPXNS).getChildren(GPXTRACKPOINTTAG, GPXNS))
		{
			Date gpxDate;
			try
			{
				gpxDate = GPXTRACKPOINTDATETIMEFORMATTER.parse(element.getChildText(GPXTIMETAG, GPXNS));
			}
			catch (ParseException e)
			{
				throw new RuntimeException("Error in trackpoint date in GPX. " + GPXTRACKPOINTDATETIMEFORMATTER.toPattern() + e);
			}
			if (gpxWaypointDate.equals(gpxDate))
			{
				gpxWaypointElement.setAttribute(GPXLATITUDINALATTRIBUTE, element.getAttributeValue(GPXLATITUDINALATTRIBUTE));
				gpxWaypointElement.setAttribute(GPXLONGITUDINALATTRIBUTE, element.getAttributeValue(GPXLONGITUDINALATTRIBUTE));
				return gpxWaypointElement;
			}
		}
		throw new RuntimeException(desc + " not fouund in the track");
	}

	/**
	 * Changes the order of the longitudinal and latitudinal coordinates and cut it after 6 digits
	 * 
	 * @param coordinateElement to change
	 * @return
	 */
	private String convertCoordinates(final Element coordinateElement)
	{
		return new BigDecimal(coordinateElement.getAttributeValue(GPXLONGITUDINALATTRIBUTE)).setScale(6, RoundingMode.HALF_UP) + "," + new BigDecimal(coordinateElement.getAttributeValue(GPXLATITUDINALATTRIBUTE)).setScale(6, RoundingMode.HALF_UP);
	}

	/**
	 * Creates a KML placemark according to KML format from a GPX waypoint
	 * 
	 * @param gpxWaypointElement the representative of the placemark in the GPX
	 * @return
	 */
	private Element createKMLPlacemark(final Element gpxWaypointElement)
	{
		Element placemarkElement = new Element(KMLPLACETAG, KMLNS);

		Element nameElement = new Element(NAMETAG, KMLNS);
		nameElement.setText(gpxWaypointElement.getChildText(NAMETAG, GPXNS));
		placemarkElement.addContent(nameElement);

		Element descriptionElement = new Element(KMLDESCRIPTIONTAG, KMLNS);
		descriptionElement.setText(gpxWaypointElement.getChildText(NAMETAG, GPXNS));
		placemarkElement.addContent(descriptionElement);

		Element styleElement = new Element(KMLSTYLEURLTAG, KMLNS);
		styleElement.setText("#" + KMLICONNAME);
		placemarkElement.addContent(styleElement);

		Element pointElement = new Element(KMLPOINTTAG, KMLNS);
		Element coordinateElement = new Element(KMLCOORDINATESTAG, KMLNS);
		coordinateElement.addContent(convertCoordinates(gpxWaypointElement));
		pointElement.addContent(coordinateElement);
		placemarkElement.addContent(pointElement);
		return placemarkElement;
	}

	/**
	 * Creates a line style element in the KML
	 * 
	 * @param route the color of the route
	 * @return
	 */
	private Element lineStyleElement(RouteEnum route)
	{
		Element styleElement = new Element(KMLSTYLETAG, KMLNS);
		styleElement.setAttribute("id", route.name());
		Element lineStyle = new Element(KMLLINESTYLETAG, KMLNS);
		styleElement.addContent(lineStyle);
		Element colorElement = new Element(KMLCOLORTAG, KMLNS);

		switch (route)
		{
			case yellowRoute:
				colorElement.setText("FF00FFFF");
				break;
			case redRoute:
				colorElement.setText("FF0000FF");
				break;
			case greenRoute:
				colorElement.setText("FF008000");
				break;
			case blueRoute:
				colorElement.setText("FFFF0000");
				break;
			case blackRoute:
				colorElement.setText("FF000000");
				break;
			default:
				colorElement.setText("FF000000");
				break;
		}
		lineStyle.addContent(colorElement);
		Element widthElement = new Element(KMLWIDTHTAG, KMLNS);
		widthElement.setText("4");
		lineStyle.addContent(widthElement);

		return styleElement;
	};
}
